<?php
/**
 * Model Base Class
 * Deals with Database mapping with objects
 * @method Model find_by_*() find_by_*(mixed $value, array $options = array()) * = attribute name. Finds rows where given attribute equals given value.
 * @package PersonalHomePageFramework
 * @subpackage Model
 * @category Core
 * @author Rolands Atvars <rolands.atvars@gmail.com>
 * @copyright Copyright (c) 2009, Rolands Atvars
 * @license http://opensource.org/licenses/gpl-license.php GNU General Public License version 3 (GPLv3)
 */
abstract class ModelBase{
    /**
     * @var string child class name
     * @access protected
     */
    protected $class_name;
    /**
     * @var string current models table name in database
     * @access protected
     */
    protected $table_name;
    /**
     * @var ResultObject|array contains rows form given SELECT query
     * @access protected
     */
    protected $result;
    /**
     * @var int currently selected row if $result is an array
     * @access protected
     */
    protected $row_set_iterator;
    /**
     * @var array current models attribute names in database table
     * @access protected
     */
    protected $attributes;
    /**
     * @var array current models table attribute names together with their values in format array("name" => "value")
     * @access protected
     */
    protected $data;
    /**
     * @var array current models one-to-many association. Contains other Models names.
     * @access protected
     */
    protected $has_many = array();
    
    /**
     * Initializes current Model.
     * @param array $params optional values for current Model. Must be an associative array in format array("name" => "value") Defaults to null.
     * @access public
     */
    public function __construct($params = null) {
        $this->class_name();
        $this->table_name();
        $this->init($params);
    }
    
    /**
     * Gets current Models attribute or child Model or one-to-many model.
     * Cannot be called directly. Fires when you try to access Models attribute.
     * @param string $name attributes name
     * @return mixed
     * @access public
     */
    public function __get($name) {
      if(isset($this->data[$name])) {
        return $this->data[$name];
      }
      
      if(!empty($this->data["id"]) and $this->is_id_of_model($name)) {
        $class = camelcasify($name);
        $class = new $class();
        $value = $class->find($this->data[$name . "_id"], array("override_error" => true));
        $this->set_attribute($name, $value);
        return $this->data[$name];
      }
      
      if($this->attribute_exists($name)) {
        return $this->data[$name];
      }
      
      if(!empty($this->data["id"]) and !empty($this->data["id"])) {
        foreach($this->has_many as $class) {
          $where = $this->table_name . "_id = " . $this->data["id"];
          $class_name = $class;
          $class = new $class_name;
          $class = $class->all(array("where" => $where));
          $this->set_attribute($name, $class);
          return $this->data[$name];
        }
      }
      
      return null;
    }
    
    /**
     * Sets Models attribute with given value.
     * Cannot be called directly. Fires when you set Models attribute.
     * @param string $name attributes name
     * @param mixed $value attributes value
     * @access public
     */
    public function __set($name, $value) {
      if(isset($this->data[$name]) or $this->attribute_exists($name)) {
        $this->set_attribute($name, $value);
      }
      
      if($this->is_id_of_model($name)) {
        $this->set_attribute($name, $value);
        $this->set_attribute($name . "_id", $value->id);
      }
    }
    
    public function __call($name, $attributes) {
      global $db;
      
      if(substr($name, 0, 8) == "find_by_") {
        $name = substr($name, 8);
        if($this->attribute_exists($name)) {
          $value = $attributes[0];
          $db->escape(&$value);
          $where = $name . ' = \'' . $value . '\'';
          $attributes[1] = (isset($attributes[1])) ? $attributes[1] : array();
          $options = array_merge(array("where" => $where), $attributes[1]);
          $object = $this->all($options);
          return $object;
        }
      }
      
      return null;
    }
    
    /**
     * Sets attributes $class_name value with current Models class name
     * @access public
     */
    public function class_name() {
        $class_name = get_class($this);
        $this->class_name = $class_name;
    }
    
    /**
     * Sets attributes $table_name value with current Models table name in database
     * @access public
     */
    public function table_name() {
      $this->table_name = underscorify($this->class_name);
    }
    
    /**
     * Prepares model for initialization.
     * @param array $params attribute values
     * @access protected
     */
    protected function init($params) {
        global $db;
        
        $this->clear_data();
        $this->set_fields();
        
        if($params != null) {
            $this->set($params);
        }
    }
    
    /**
     * Initializes Models attributes from database.
     * @access protected
     */
    protected function set_fields() {
      global $db;
      
      $fields = $db->fields($this->table_name);
      foreach($fields as $field) {
        $this->attributes[] = $field;
        $this->set_attribute($field, "");
      }
    }
    
    /**
     * Clears Models attribute values.
     * @access protected
     */
    protected function clear_data() {
      $this->data = array();
    }
    
    /**
     * Checks if given Models attribute exists.
     * @param string $name Models attribute name
     * @return bool
     * @access protected
     */
    protected function attribute_exists($name) {
      foreach($this->attributes as $attribute) {
        if($name == $attribute) {
          return true;
        }
      }
      return false;
    }
    
    /**
     * Sets Models attribute with given value.
     * @param string $name
     * @param mixed $value
     * @access protected
     */
    protected function set_attribute($name, $value) {
      if($this->attribute_exists($name) or $this->is_id_of_model($name) or $this->is_in_has_many($name)) {
        $value = auto_stripslashes($value);
        $this->data[$name] = $value;
      }
    }
    
    /**
     * Gets Models attribute from given name.
     * @param string $name
     * @return mixed
     * @access protected
     */
    protected function get_attribute($name) {
        return $this->data[$name];
    }
    
    /**
     * Tells if given Models attribute is an id for different Model
     * @param string $key Model attributes name
     * @return bool
     * @access protected
     */
    protected function is_id_of_model($key) {
      $key .= "_id";
      foreach($this->attributes as $attribute) {
        if($key == $attribute) {
          return true;
        }
      }
      return false;
    }
    
    /**
     * Tells if given Models attribute is another Model (one-to-many)
     * @param string $name
     * @return bool
     * @access protected
     */
    protected function is_in_has_many($name) {
      $name = camelcasify($name);
      foreach($this->has_many as $class) {
        if($class == $name) {
          return true;
        }
      }
      return false;
    }
    
    /**
     * Sets Models attributes with given values.
     * @param array $params Models attribute names with values in form array("name" => "value")
     * @access public
     */
    public function set($params) {
        foreach($params as $key => $value) {
            $this->set_attribute($key, $value);
        }
    }
    
    /**
     * Gets Models rows from database from given options
     * @global $db
     * @param array $options optional query options. where, limit, order_by
     * @return Model|null Model with all the rows in $result array. Returns null in case of exception.
     * @throws Could not perform query exception if there was an error in generated query. If Production mode is on renders 500 error.
     * @access public
     */
    public function all($options = array()) {
        global $db;
        $order_by = (empty($options["order_by"])) ? null : $options["order_by"];
        $where = (empty($options["where"])) ? null : $options["where"];
        $limit = (empty($options["limit"])) ? null : $options["limit"];
        
        try {
            $result = $db->select($this->table_name, "*", $where, null, $order_by, $limit);
        }
        catch(Exception $e) {
            $error = "Could not get all %s:<br />Details:%s";
            $error = sprintf($error, $this->class_name, $e->getMessage());
            if(IS_PRODUCTION) {
                render_500_error();
            }
            else {
                throw new Exception($error);
            }
            return null;
        }
        $this->result = $result;
        return $this;
    }
    
    /**
     * Finds Model from database with given id.
     * @global $db
     * @param int $id
     * @param array $options optional
     * @return Model|null found Model with attributes set or returns null if Model with given id does not exist.
     * @throws Model not found exception if model with given id does not exist. Renders 500 page is Production mode is on.
     * @access public
     */
    public function find($id, $options = array()) {
        global $db;
        
        $db->escape(&$id);
        
        try {
            $result = $db->select($this->table_name, "*", "id = $id");
        }
        catch(Exception $e) {
            $error = "Error while finding %s with id: $id<br />Details: %s";
            $error = sprintf($error, $this->class_name, $e->getMessage());
            if($options["override_error"] !== true) {
                if(IS_PRODUCTION) {
                    render_500_page();
                }
                else {
                    throw new Exception($error);
                }
            }
            return null;
        }
        
        if(mysqli_num_rows($result) != 0) {
            $row = mysqli_fetch_assoc($result);
            $this->init($row);
            return $this;
        }
        else {
            if(IS_PRODUCTION) {
                render_404_error();
            }
            else {
                throw new Exception("Object (" . $this->class_name . ") with id: <b>$id</b>, wasn't found!");
            }
            return null;
        }
    }
    
    /**
     * Creates Model in database. (Performs INSERT)
     * @global $db
     * @return bool returns true on successful INSERT
     * @throws Failed To Create exception if failed to create Model in database. Renders 500 error if safe mode is on.
     * @access public
     */
    public function create() {
        global $db;
        $variables = array();
        $values = array();
        
        foreach($this->attributes as $variable) {
            $variables[] = $variable;
            $values[] = $this->get_attribute($variable);
        }
        
        try {
            $id = $db->insert($this->table_name, $variables, $values);
        }
        catch(Exception $e) {
            $error = "Failed to create new %s.<br />Details: %s";
            $error = sprintf($error, $this->class_name, $e->getMessage());
            if(IS_PRODUCTION) {
                render_500_error();
            }
            else {
                throw new Exception($error);
            }
            return false;
        }
        
        if($id) {
            $this->id = $id;
            return true;
        }
        else {
            if(IS_PRODUCTION) {
                render_500_error();
            }
            else {
                $error = "Failed to get created %s id!";
                $error = sprintf($error, $this->class_name);
                throw new Exception($error);
            }
            return false;
        }
    }
    
    /**
     * Saves Models changes in database. (Performs UPDATE)
     * @global $db
     * @return bool returns true on success
     * @throws Failed To Update Exception if failed to update Model in database. Renders 500 error if Production mode is on.
     * @access public
     */
    public function update() {
        global $db;
        $values = array();
        
        foreach($this->attributes as $variable) {
            $values[] = $this->get_attribute($variable);
        }
        
        $where = "id = '" . $this->get_attribute("id") . "'";
        
        try {
            $db->update($this->table_name, $this->attributes, $values, array("where" => $where));
        }
        catch(Exception $e) {
            $error = "Failed to update %s.<br />Details: %s";
            $error = sprintf($error, $this->class_name, $e->getMessage());
            if(IS_PRODUCTION) {
                render_500_error();
            }
            else {
                throw new Exception($error);
            }
            return false;
        }
        
        return true;
    }
    
    /**
     * Destroys Model in database. (Performs DELETE)
     * @global $db
     * @return bool returns true on success
     * @throws Failed To Delete exception if failed to perform query on database. Renders 500 error if safe mode is on.
     * @access public
     */
    public function destroy() {
      global $db;
      
      try {
        $db->delete($this->table_name, "id = " . $this->id);
      }
      catch(Exception $e) {
        $error = "Failed to delete %s.<br />Details: %s";
        $error = sprintf($error, $this->class_name, $e->getMessage());
        if(IS_PRODUCTION) {
          render_500_error();
        }
        else {
          throw new Exception($error);
        }
        return false;
      }
      return true;
    }
    
    /**
     * Saves all the Models rows to an array from ResultObject.
     * @access public
     */
    public function row_set_to_array() {
      $tmp_result = array();
      while($row = $this->result->fetch_assoc()) {
        $tmp_result[] = $row;
      }
      $this->result = $tmp_result;
      $this->row_set_iterator = 0;
    }
    
    /**
     * Gets the next row from given result set.
     * @return Model|bool returns Model with Model attributes from next row or returns false if there are no more rows left.
     * @access public
     */
    public function next() {
      if(is_array($this->result)) {
        if(isset($this->result[$this->row_set_iterator])) {
          $this->init($this->result[$this->row_set_iterator]);
          $this->row_set_iterator++;
          return $this;
        }
        else {
          $this->row_set_iterator = 0;
          return false;
        }
      }
      else {
        $row = mysqli_fetch_assoc($this->result);
        if($row == false) {
          return false;
        }
        else {
          $this->init($row);
          return $this;
        }
      }
    }
    
    /**
     * Gets how many rows are there from given query or already in result set.
     * @global $db
     * @param array $options optional options for SQL query
     * @return int row count
     * @access public
     */
    public function count($options = array()) {
      if(isset($this->result)) {
        if(is_array($this->result)) {
          return count($this->result);
        }
        else {
          $count = $this->result->num_rows;
        }
      }
      else {
        global $db;
        
        $count = $db->count($this->table_name, $options);
      }
      return $count;
    }
}
?>